Skip to content

Add enumflags2 for DeviceCapability enum#11

Merged
ids1024 merged 10 commits intoids1024:masterfrom
axelkar:seat-cap-bitflags
Aug 6, 2025
Merged

Add enumflags2 for DeviceCapability enum#11
ids1024 merged 10 commits intoids1024:masterfrom
axelkar:seat-cap-bitflags

Conversation

@axelkar
Copy link
Contributor

@axelkar axelkar commented Jul 15, 2025

This removes the need for slices and Vecs, and makes client and server code a lot simpler to implement with BitFlags::contains and friends.

@ids1024
Copy link
Owner

ids1024 commented Jul 15, 2025

wayland-rs uses bitflags. Not sure what reasons there might be to prefer one over the other between that and enumflags2.

@axelkar
Copy link
Contributor Author

axelkar commented Jul 16, 2025

I originally based this PR on bitflags, but changed to enumflags2 because you can get enums in the iterator. It means less expect and unreachable!. I'd strongly prefer that, especially for an API.

I think wayland-rs might use bitflags because it automatically supports storing unknown bits. With enumflags2, it definitely is possible by parsing FromBitsError and storing unknown bits separately, but it's not the main use case. If there's a const _ = !0 in bitflags! in wayland-rs, it means that the generated struct will store unknown bits.

@axelkar
Copy link
Contributor Author

axelkar commented Jul 16, 2025

As the server, we don't need to store unknown bits, because we control the meaning of the bits. As the client, we won't do anything with bits we don't know the meaning of; that's why the error for an unknown interface.

As a maintainer, that means that when you advertise support for a device capability interface in the handshake, you need to add it to the enum. Nothing else really.

@ids1024
Copy link
Owner

ids1024 commented Jul 18, 2025

I think wayland-rs might use bitflags because it automatically supports storing unknown bits. With enumflags2, it definitely is possible by parsing FromBitsError and storing unknown bits separately, but it's not the main use case. If there's a const _ = !0 in bitflags! in wayland-rs, it means that the generated struct will store unknown bits.

Wayland-rs doesn't currently use bitflags this way for storing unknown bits (at least for things it receives), but it probably should: Smithay/wayland-rs#793

It seems with wayland, it's valid with some enums but not others to advertise a value that is not advertised in the version of the protocol in use: https://gitlab.freedesktop.org/wayland/wayland/-/issues/497, though that's has never been clearly specified.

With libei, it looks like https://gitlab.freedesktop.org/libinput/libei/-/blob/main/proto/protocol.dtd and https://gitlab.freedesktop.org/libinput/libei/-/blob/main/proto/ei-scanner do define a bitfield property, but it's not currently used anywhere in https://gitlab.freedesktop.org/libinput/libei/-/blob/main/proto/protocol.xml. So we may need to generate bitflags of some sort for the protocol in the future, but not currently. (And if we do, would we also want to allow unrecognized bits, as with Wayland?)

Though reis doesn't promise anything about avoiding API breaks (and is unlikely to ever be as much of a pain to deal with as wayland-rs API breaks), so it's not a huge problem if we have to change this in the future.

I originally based this PR on bitflags, but changed to enumflags2 because you can get enums in the iterator.

What exactly is different here? https://docs.rs/bitflags/latest/bitflags/example_generated/struct.Flags.html shows iter() and iter_names() methods.

@axelkar
Copy link
Contributor Author

axelkar commented Jul 18, 2025

And if we do, would we also want to allow unrecognized bits, as with Wayland?

For the capability stuff, as the client, the opaque/dynamic masks are stored in capability_map. The server decides the meaning of each bit in the capability masks.

For bitmasks in the protocol, it really depends on the definition. Probably yes.

With enumflags2, you can simply store unknowns next to the known flags, like so:

#[enumflags2::bitflags]
#[repr(u64)]
enum MyFlag {}

struct ProtoMyFlags {
  known: enumflags2::BitFlags<MyFlag>,
  unknown: u64
}

With a PR, enumflags2::BitFlags could probably be made store unknowns within the same integer.

You can always do this too:

#[enumflags2::bitflags]
#[repr(u64)]
enum MyFlag {}

struct ProtoMyFlags(u64);

impl ProtoMyFlags {
  fn iter(&self) -> enumflags2::Iter {
    BitFlags::<MyFlag>::from_bits_truncate(self.0).iter()
  }
  fn has_flag(&self, flag: MyFlag) {
    BitFlags::<MyFlag>::from_bits_truncate(self.0).contains(flag)
  }
  fn get_unknown(&self) -> u64 {
    self.0 & !BitFlags::<MyFlag>::ALL.bits()
  }
}

I originally based this PR on bitflags, but changed to enumflags2 because you can get enums in the iterator.

What exactly is different here? https://docs.rs/bitflags/latest/bitflags/example_generated/struct.Flags.html shows iter() and iter_names() methods.

With bitflags, you don't get a static guarantee (like a Rust enum) that iter_names gives you valid flags. I.e.

flags.iter_names(|flag| match flag.bits() {
  Self::A => do_thing(),
  _ => unreachable!() // the compiler forces this on you!
})

fn interface_name(&self) -> Option<&'static str> {
  match self.bits() {
    Self::A => Some("a"),
    _ => None // same thing, and here the library isn't even promising that `self` is always a single valid flag
  }
}

Compare to the equivalent with enumflags2:

flags.iter(|flag| match flag {
  Self::A => do_thing(),
  // It's a Rust enum so we know this is exhaustive
})

fn interface_name(&self) -> &'static str {
  match self {
    Self::A => "a",
    // same thing
  }
}

While writing this, I got a reply to meithecatte/enumflags2#66. enumset looks way better for the use case of contiguous reprs.

@ids1024
Copy link
Owner

ids1024 commented Jul 20, 2025

Ah right. So basically enumflags2 makes the individual flag an enum (as is in the name) and district from the Bitflags<T> containing it. Fair enough.

I think enumflags2 should be fine. If needed we can change this later anyway. This at least needs a cargo fmt to pass CI.

@axelkar axelkar force-pushed the seat-cap-bitflags branch 2 times, most recently from 8f4a39d to 049ca9d Compare July 23, 2025 12:59
@axelkar
Copy link
Contributor Author

axelkar commented Jul 23, 2025

Ugh.. Is there an MSRV of 1.70.0? Shouldn't it be specified in the Cargo.toml?

@ids1024
Copy link
Owner

ids1024 commented Jul 25, 2025

Shouldn't it be specified in the Cargo.toml?

rust-version = "1.70.0"

@axelkar axelkar force-pushed the seat-cap-bitflags branch 2 times, most recently from ccce246 to ae01709 Compare July 28, 2025 21:56
@axelkar
Copy link
Contributor Author

axelkar commented Jul 28, 2025

Sorry for the delay! I wondered why Rust or Clippy didn't check the MSRV: rust-lang/compiler-team#772

Can you help fix the libei test? I don't see how I broke it

@ids1024
Copy link
Owner

ids1024 commented Aug 1, 2025

Running the tests locally, it seems test/test_protocol.py::TestEiProtocol::test_connect_receive_seat is failing because that tests expects the server to advertise the EI_SCROLL and EI_BUTTON capabilities, but is missing those from the list of interfaces the client negotiates. Adding those to the setup.InterfaceVersion loop fixes it: https://gitlab.freedesktop.org/libinput/libei/-/blob/main/test/test_protocol.py?ref_type=heads#L554-630

So that breaks with the self.has_interface() test added in add_seat.

Then there's also test/test_protocol.py::TestEiProtocol::test_seat_bind_invalid_caps_expect_disconnection failing. That is from removing the capabilities & 0x7e != capabilities test from reis-demo-server. So we want some version of that check to pass tests.

@ids1024
Copy link
Owner

ids1024 commented Aug 1, 2025

@axelkar axelkar force-pushed the seat-cap-bitflags branch 2 times, most recently from cf12669 to 01f8ff6 Compare August 1, 2025 21:41
@axelkar
Copy link
Contributor Author

axelkar commented Aug 1, 2025

Seems that CI doesn't run cargo test. It was failing from a doc-test, so I fixed it.

@axelkar
Copy link
Contributor Author

axelkar commented Aug 1, 2025

Still fails with the below??

=========================== short test summary info ============================
FAILED test/test_protocol.py::TestEiProtocol::test_seat_bind_invalid_caps_expect_disconnection - AssertionError: Expected seat to get destroyed but didn't
assert False
======================== 1 failed, 38 passed in 22.96s =========================

@ids1024
Copy link
Owner

ids1024 commented Aug 2, 2025

The test passes for me with these two changes:

diff --git a/src/event.rs b/src/event.rs
index e042fc0..4728acf 100644
--- a/src/event.rs
+++ b/src/event.rs
@@ -680,17 +680,17 @@ pub struct Keymap {
 #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
 pub enum DeviceCapability {
     /// Capability for relative pointer motion.
-    Pointer = 1 << 0,
+    Pointer = 1 << 1,
     /// Capability for absolute pointer motion.
-    PointerAbsolute = 1 << 1,
+    PointerAbsolute = 1 << 2,
     /// Capability for keyboard input events.
-    Keyboard = 1 << 2,
+    Keyboard = 1 << 3,
     /// Capability for touch input events.
-    Touch = 1 << 3,
+    Touch = 1 << 4,
     /// Capability for scroll input events.
-    Scroll = 1 << 4,
+    Scroll = 1 << 5,
     /// Capability for mouse button input events.
-    Button = 1 << 5,
+    Button = 1 << 6,
 }

 impl DeviceCapability {
diff --git a/src/request.rs b/src/request.rs
index 4b08a3f..fb0e90c 100644
--- a/src/request.rs
+++ b/src/request.rs
@@ -27,7 +27,7 @@ pub enum RequestError {
 impl fmt::Display for RequestError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         match self {
-            Self::InvalidCapabilities => write!(f, "invalid capabilities"),
+            Self::InvalidCapabilities => write!(f, "Invalid capabilities"),
         }
     }
 }

The use of ei.send(seat.Bind(0x1)) in the test should probably be change to find the first bit that is not used the compositor for any capability, instead of assuming the first bit is unused by capabilities. But I don't think the specific bitflag values would be considered a stable API of reis here, so we can just skip using that bit for now.

Comment on lines 127 to 142
Err(err) => {
if let reis::Error::Request(reis::request::RequestError::InvalidCapabilities) =
err
{
Ok(context_state.disconnected(
connected_state,
eis::connection::DisconnectReason::Value,
&err.to_string(),
))
} else {
Ok(context_state.protocol_error(connected_state, &err.to_string()))
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ids1024 What do you think of this? Is this still required with the changes you mention?

@axelkar
Copy link
Contributor Author

axelkar commented Aug 2, 2025

The test passes for me with these two changes:

Modifying DeviceCapability shouldn't do anything because we have the capability map. That's really weird.

@axelkar
Copy link
Contributor Author

axelkar commented Aug 2, 2025

What do you think of RUSTDOCFLAGS="--deny warnings" cargo doc --no-deps --all-features and --all-targets for Clippy so it lints the examples in CI? CI had missed the broken intra-doc link.

@ids1024
Copy link
Owner

ids1024 commented Aug 4, 2025

Modifying DeviceCapability shouldn't do anything because we have the capability map. That's really weird.

The libei tests are just testing the server-side implementation (reis-demo-server; which is designed to match libei's eis-demo-server and pass the test suite). While CapabilityMap is client-side.

What do you think of RUSTDOCFLAGS="--deny warnings" cargo doc --no-deps --all-features and --all-targets for Clippy so it lints the examples in CI? CI had missed the broken intra-doc link.

Yeah, sounds good to have CI test that.

@axelkar
Copy link
Contributor Author

axelkar commented Aug 4, 2025

Modifying DeviceCapability shouldn't do anything because we have the capability map. That's really weird.

The libei tests are just testing the server-side implementation (reis-demo-server; which is designed to match libei's eis-demo-server and pass the test suite). While CapabilityMap is client-side.

I forgot I used event::DeviceCapability in the request module as well. Oops.

The use of ei.send(seat.Bind(0x1)) in the test should probably be change to find the first bit that is not used the compositor for any capability, instead of assuming the first bit is unused by capabilities. But I don't think the specific bitflag values would be considered a stable API of reis here, so we can just skip using that bit for now.

Ohhh now I get it... libei/test/test_protocol.py hardcodes that the bit value 0x1 must be invalid, instead of listening to ei_seat.capability events. I don't see it as an error on reis's side as the protocol states that the server controls the meaning of every bit.

@axelkar
Copy link
Contributor Author

axelkar commented Aug 4, 2025

axelkar added 9 commits August 5, 2025 02:22
This removes the need for slices and `Vec`s, and makes client and server
code a lot simpler to implement with `BitFlags::contains` and friends.
Notes:
- Exact command done: `cargo clippy --fix --features calloop,tokio -- -W clippy::pedantic`
- Undid fixes for auto-generated files, for now
- Ran `cargo fmt`
Not literally everything, but almost; some things aren't perfect and
a couple of enums are ignored.
@axelkar axelkar force-pushed the seat-cap-bitflags branch from dca100e to 0db1cd5 Compare August 4, 2025 23:35
@axelkar axelkar force-pushed the seat-cap-bitflags branch from 0db1cd5 to 2eb802f Compare August 4, 2025 23:45
@ids1024
Copy link
Owner

ids1024 commented Aug 5, 2025

Re-running the CI job now that libei PR is merged, it passes.

@axelkar
Copy link
Contributor Author

axelkar commented Aug 5, 2025

Great! Is this ready for merge?

Copy link
Owner

@ids1024 ids1024 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking over everything here again, it seems good.

@ids1024 ids1024 merged commit f6f0c1f into ids1024:master Aug 6, 2025
5 of 6 checks passed
whot added a commit to whot/ashpd that referenced this pull request Feb 16, 2026
bilelmoussaoui pushed a commit to bilelmoussaoui/ashpd that referenced this pull request Feb 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants